fix(backends): honor inherited model label + warn on engine/model incompatibility - #1533
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
nhopeatall
left a comment
There was a problem hiding this comment.
Summary
APPROVE — this makes the model picker honest about the actual override → per-agent → project.model resolution chain (no engine-default step) and adds a config-time engine/model incompatibility warning. All six MNG-1772 steps are implemented; the resolveClaudeModel / resolveCodexModel refactor is behavior-preserving; web + backend typechecks and the new unit/guard tests all pass.
What I verified
- Behavior-preserving refactor — the
CLAUDE_CODE_ACCEPTED_PREFIXES/CODEX_ACCEPTED_PREFIXESrewrite of the inlinestartsWithchecks is a faithful translation (anthropic:still strips the prefix;openai:still requires a catalog-id bare match). 194 existingclaude-code/codextests stay green. - Serialization —
acceptedModelPrefixesis a plain (readonly)string[], so it survives theagentConfigs.enginestRPC query, and the FEisModelCompatibleWithEnginemirror reads it.enginesis castas Engine[]from the query data, and the runtime catalog populates the field. - No call-site regressions — the three
ModelFieldsites are consistent: the global settings dialog (agent-config-form-dialog.tsx) passes nodefaultLabel(unchanged engine-default fallback), while the detail panel and harness form pass inheritance-aware labels. - Anti-drift guard —
catalog.test.tsassertslabelis defined anddefaultValueLabel === Default (<label>)for eachselectengine, so bumpingDEFAULT_*_MODELto a non-catalog id fails loudly. - Warning fires on the target case — claude-code inheriting
openrouter:google/gemini-3-flash-previewcorrectly surfaces the amber warning; catalog /claude-/anthropic:models clear it.
Code Issues
Nitpick (non-blocking)
- agent-config-detail.tsx:252 — the warning copy always reads "Inherited model X" even when X is an explicit per-agent override rather than an inherited value. This is reachable: with the engine left as "Inherit" (
agentEngine=''),ModelFieldrenders the free-text OpenRouter combobox, so a user can set an incompatible override; theneffectiveModel = model(the override) but the copy still says "Inherited". Wording like "Model X will fail on the {engine} engine" stays accurate in both the inherited and explicit-override cases. Purely cosmetic — the signal itself is correct and actionable.
🕵️ claude-code · claude-opus-4-8 · run details
| <p className="flex items-start gap-1.5 text-xs text-amber-600 dark:text-amber-400"> | ||
| <AlertTriangle className="mt-0.5 h-3.5 w-3.5 shrink-0" aria-hidden="true" /> | ||
| <span> | ||
| Inherited model <strong>{effectiveModel}</strong> will fail on the{' '} |
There was a problem hiding this comment.
Non-blocking wording nit: this always says "Inherited model" even when effectiveModel is an explicit per-agent override. When the engine is left as "Inherit" (agentEngine=''), ModelField renders the free-text OpenRouter combobox (the select branch is skipped for an empty engine id), so a user can set an incompatible override — effectiveModel = model || inheritedModel resolves to the override, yet the copy calls it "Inherited". Consider "Model {effectiveModel} will fail on the {engine} engine" so it reads correctly in both the inherited and explicit-override cases.
Summary
Fixes MNG-1772. The agent detail panel's Model picker showed a fictional fallback (
Default (Sonnet 5)) that the runtime never applies. The runtime chain (src/agents/shared/modelResolution.ts) isoverride → per-agent → project.modelwith no engine-default step, so an empty override actually inheritsproject.model— which defaults toopenrouter:google/gemini-3-flash-previewand then fails theclaude-codeengine guard mid-run. This PR makes the UI honest and surfaces the incompatibility at configuration time.What changed
1. Honor
defaultLabelon theModelFieldselect branch (web/src/components/settings/model-field.tsx)resolveSelectEmptyLabel(defaultLabel, engineDefaultValueLabel)→defaultLabel ?? engineDefaultValueLabel._none) option now renders the caller's inheritance-aware label (Inherit from project (X)) instead of the engine catalog's hardcoded default — matching the agents list. The_none → ''save mapping is unchanged.2. Derive
defaultValueLabelfrom the default-model constants (src/backends/catalog.ts,claude-code/models.ts,codex/models.ts)defaultModelLabel(models, defaultId)→Default (${label ?? id}).'Default (Sonnet 5)'/'Default (GPT-5.4)'literals with values derived fromDEFAULT_CLAUDE_CODE_MODEL/DEFAULT_CODEX_MODEL+ catalog lookup, killing the hand-sync drift class.3. Normalize the project harness-form model default label (
web/src/components/projects/project-harness-form.tsx)Default (${defaults.model})for a clear empty-option read.4. Config-time engine/model incompatibility warning (
src/backends/*,web/src/components/projects/*)CLAUDE_CODE_ACCEPTED_PREFIXES = ['claude-', 'anthropic:']/CODEX_ACCEPTED_PREFIXES = ['openai:']next to the model lists, consumed byresolveClaudeModel/resolveCodexModel(no behavior change).modelSelection.acceptedModelPrefixes?: readonly string[](plain array — serializes across theagentConfigs.enginestRPC query).isModelCompatibleWithEngine(model, engine)(catalog-membership OR accepted-prefix match; free-text/unknown engines always compatible).agentEngine || inheritedEngine) + effective model (model || inheritedModel) and shows an inline amberAlertTrianglewarning when aselect-type engine would fail on the inherited model. Turns a post-dispatch crash into a config-time signal.5. Tests
resolveSelectEmptyLabel,isModelCompatibleWithEngine,defaultModelLabelunit tests.tests/unit/backends/catalog.test.tsasserting displayed default == resolved default for everyselectengine — fails loudly on the next model bump.resolveClaudeModel/resolveCodexModeltests remain green (refactor is behavior-preserving).6. Docs —
docs/adding-engines.mdmodelSelectionexample now derivesdefaultValueLabelfrom the default-model constant and documents the new optionalacceptedModelPrefixes.Scope notes
modelResolution.ts(that is the direction-4 product decision). This PR makes the UI honest about the existing chain.PROJECT_DEFAULTSclaude-code + openrouter default mismatch is flagged by the new warning rather than silently changed — a follow-up product call.Testing
npm run typecheck(root) +tsc -b(web) — cleanbiome checkon changed files — cleannpx vitest runforcatalog.test.ts,model-field.test.ts,agent-config-utils.test.ts,claude-code.test.ts,codex.test.ts,engine-contract,registry,resolution, alltests/unit/web/, andarchitecture-docs.test.ts— all passing🤖 Generated with Claude Code
🕵️ claude-code · claude-opus-4-8 · run details